﻿// Original author contact info: Owen Emlen (owene_1998@yahoo.com)
// Note: other individuals may also have contributed to this code
// Project hosted on CodePlex.com as of 1/10/2009 at http://www.codeplex.com/EmlenMud
using System.Runtime.Serialization;
using System;
using System.Collections;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Xml.Serialization;
#if USE_HYPER
using Hyper.ComponentModel;
#endif

namespace BrainTechLLC.ThreadSafeObjects
{
    /// <summary>
    /// Thread-safe lookup allows multiple threads to look up items by key, add items,
    /// and remove items in a thread-safe manner
    /// </summary>
#if NO_SILVERLIGHT
    [TypeDescriptionProvider(typeof(HyperTypeDescriptionProvider))]
    [EditorBrowsable(EditorBrowsableState.Always)]
#endif
    [Serializable]
    [DataContract]
    public class ThreadSafeLookup<TIndex, TListType> : Lockable, IDictionary<TIndex, TListType>, IMultipleItems<TListType>,
        ISupportsCount,
        INotifyCollectionChanged,
        ISupportsAddRemoveClear<TIndex, TListType> where TListType : class
    {
        public override string ToString()
        {
            return Count.ToString();
        }

        [DataMember]
        public Dictionary<TIndex, TListType> Lookup;

#if NO_SILVERLIGHT
        [EditorBrowsable(EditorBrowsableState.Always)]
#endif
        [XmlIgnore]
        public TListType[] ArrayOfItems { get { return AllItems.ToArray(); } }

#if NO_SILVERLIGHT
        [EditorBrowsable(EditorBrowsableState.Always)]
#endif
        [XmlIgnore]
        public int Count { get { return Lookup.Count; } }

        public bool ContainsKey(TIndex key) { return Lookup.ContainsKey(key); }
        public bool TryGetValue(TIndex key, out TListType items) { return Lookup.TryGetValue(key, out items); }

        public ThreadSafeLookup<TIndex, TListType> Merge<T>(ThreadSafeLookup<TIndex, T> other) where T : class, TListType
        {
            var kvps = other.AllKeyValuePairs;
            AquireLock();
            foreach (var kvp in kvps)
            {
                if (!Lookup.ContainsKey(kvp.Key))
                {
                    Lookup.Add(kvp.Key, kvp.Value);
                }
            }
            ReleaseLock();

            return this;
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return Lookup.GetEnumerator();
        }

        public IEnumerator<KeyValuePair<TIndex, TListType>> GetEnumerator()
        {
            return Lookup.GetEnumerator();
        }

#if NO_SILVERLIGHT
        [EditorBrowsable(EditorBrowsableState.Never), Browsable(false)]
#endif
        [XmlIgnore]
        public bool IsReadOnly { get { return false; } }

#if NO_SILVERLIGHT
        [EditorBrowsable(EditorBrowsableState.Never), Browsable(false)]
#endif
        [XmlIgnore]
        public List<TIndex> Keys
        {
            get
            {
                List<TIndex> items = new List<TIndex>(Lookup.Count);
                AquireLock(); { Lookup.ForEach(kvp => items.Add(kvp.Key)); } ReleaseLock();
                return items;
            }
        }

        public ThreadSafeLookup() { Lookup = new Dictionary<TIndex, TListType>(); }
        public ThreadSafeLookup(StringComparer comparer) { Lookup = new Dictionary<TIndex, TListType>((IEqualityComparer<TIndex>)comparer); }
        public ThreadSafeLookup(StringComparer comparer, params KeyValuePair<TIndex, TListType>[] items)
        {
            Lookup = new Dictionary<TIndex, TListType>((IEqualityComparer<TIndex>)comparer);

            foreach (var kvp in items)
                Add(kvp.Key, kvp.Value);
        }


        [EditorBrowsable(EditorBrowsableState.Advanced)]
        [XmlIgnore]
        public List<KeyValuePair<TIndex, TListType>> AllKeyValuePairs
        {
            get
            {
                List<KeyValuePair<TIndex, TListType>> results = new List<KeyValuePair<TIndex, TListType>>();
                AquireLock();
                {
                    foreach (var v in Lookup)
                        results.Add(v);
                }
                ReleaseLock();

                return results;
            }
        }

#if NO_SILVERLIGHT
        [EditorBrowsable(EditorBrowsableState.Never), Browsable(false)]
#endif
        [XmlIgnore]
        public List<TListType> AllItems
        {
            get
            {
                // TODO: caching
                List<TListType> items = new List<TListType>(Lookup.Count);
                AquireLock(); { Lookup.ForEach(kvp => items.Add(kvp.Value)); } ReleaseLock();
                return items;
            }
        }

        public bool Add(TIndex key, TListType item)
        {
            bool added = false;

            AquireLock();
            {
                if (!Lookup.ContainsKey(key))
                {
                    Lookup.Add(key, item);
                    added = true;
                }
            }
            ReleaseLock();

            if (added)
                OnNotifyCollectionChanged(NotifyCollectionChangedAction.Add, item);

            return added;
        }

        public bool AddMultipleKeys(IEnumerable<TIndex> keys, TListType item)
        {
            bool added = false;

            AquireLock();
            {
                foreach (TIndex key in keys)
                {
                    if (!Lookup.ContainsKey(key))
                    {
                        Lookup.Add(key, item);
                        added = true;
                    }
                }
            }
            ReleaseLock();

            if (added)
                OnNotifyCollectionChanged(NotifyCollectionChangedAction.Add, item);

            return added;
        }

        public void AddOrSet(TIndex key, TListType item)
        {
            if (Lookup.ContainsKey(key))
            {
                TListType old = null;
                Lookup.TryGetValue(key, out old);
                if (!old.AreEqual(item))
                {
                    Lookup[key] = item;

                    if (CollectionChanged != null)
                        OnNotifyCollectionChanged(NotifyCollectionChangedAction.Replace, new List<TListType>() { old, item });
                }
            }
            else
            {
                Add(key, item);
                OnNotifyCollectionChanged(NotifyCollectionChangedAction.Add, item);
            }
        }

        public bool AddIfNew(TIndex key, Func<TListType> func)
        {
            if (!Lookup.ContainsKey(key))
                return Add(key, func());

            return false;
        }

        [XmlIgnore]
        public TListType this[TIndex key]
        {
            get
            {
                TListType item;

                // Not totally safe for lookups that frequently add/remove items.
                // For thread safety, surround the following line with AquireLock(); / ReleaseLock();
                if (!Lookup.TryGetValue(key, out item)) item = default(TListType);

                return item;
            }
            set
            {
                AddOrSet(key, value);
            }
        }

        public bool Remove(TIndex key, TListType item)
        {
            return Remove(key);
        }

        public bool Remove(KeyValuePair<TIndex, TListType> kvp)
        {
            return Remove(kvp.Key, kvp.Value);
        }

        public bool Remove(TIndex key)
        {
            bool removed = false;

            if (!Lookup.ContainsKey(key))
                return false;

            TListType item = null;
            AquireLock();
            {
                if (Lookup.TryGetValue(key, out item))
                {
                    Lookup.Remove(key);
                    removed = true;
                }
            }
            ReleaseLock();

            if (item != null)
                OnNotifyCollectionChanged(NotifyCollectionChangedAction.Remove, item);
            return removed;
        }

        public void Clear()
        {
            IList<TListType> items = null;

            if (CollectionChanged != null)
                items = new List<TListType>(AllItems);

            AquireLock();
            {
                Lookup.Clear();
            }
            ReleaseLock();

            OnNotifyCollectionChanged(NotifyCollectionChangedAction.Reset, items);
        }

        public void OnNotifyCollectionChanged(NotifyCollectionChangedAction action, IList<TListType> changedItems)
        {
            if (CollectionChanged != null)
            {
                switch (action)
                {
                    case NotifyCollectionChangedAction.Add:
                        CollectionChanged(this, new NotifyCollectionChangedEventArgsEx<TListType>(action, changedItems));
                        break;
                    case NotifyCollectionChangedAction.Remove:
                        CollectionChanged(this, new NotifyCollectionChangedEventArgsEx<TListType>(action, changedItems));
                        break;
                    case NotifyCollectionChangedAction.Replace:
                        CollectionChanged(this, new NotifyCollectionChangedEventArgsEx<TListType>(action, changedItems[0], changedItems[1]));
                        break;
                    case NotifyCollectionChangedAction.Reset:
                        CollectionChanged(this, new NotifyCollectionChangedEventArgsEx<TListType>(action));
                        break;
                }
            }
        }

        public void OnNotifyCollectionChanged(NotifyCollectionChangedAction action, TListType changedItem)
        {
            if (CollectionChanged != null)
                OnNotifyCollectionChanged(action, new List<TListType>() { changedItem });
        }

        #region INotifyCollectionChanged Members

#if NO_SILVERLIGHT
        [field: NonSerialized]        
        public event NotifyCollectionChangedEventHandler CollectionChanged;
#else
        [field: NonSerialized]
        public event EventHandler<NotifyCollectionChangedEventArgsEx<TListType>> CollectionChanged;
#endif

        #endregion

        #region IDictionary<TIndex,TListType> Members

        void IDictionary<TIndex, TListType>.Add(TIndex key, TListType value)
        {
            this.Add(key, value);
        }

#if NO_SILVERLIGHT
        [EditorBrowsable(EditorBrowsableState.Never), Browsable(false)]
#endif
        [XmlIgnore]
        ICollection<TIndex> IDictionary<TIndex, TListType>.Keys
        {
            get { return Lookup.Keys; }
        }

#if NO_SILVERLIGHT
        [EditorBrowsable(EditorBrowsableState.Never), Browsable(false)]
#endif
        [XmlIgnore]
        public ICollection<TListType> Values
        {
            get { return Lookup.Values; }
        }

        #endregion

        #region ICollection<KeyValuePair<TIndex,TListType>> Members

        public void Add(KeyValuePair<TIndex, TListType> item)
        {
            Add(item.Key, item.Value);
        }

        public bool Contains(KeyValuePair<TIndex, TListType> item)
        {
            return Lookup.Contains(item);
        }

        #endregion

        #region ICollection<KeyValuePair<TIndex,TListType>> Members

        public void CopyTo(KeyValuePair<TIndex, TListType>[] array, int arrayIndex)
        {
            AquireLock();
            {
                foreach (KeyValuePair<TIndex, TListType> item in Lookup)
                {
                    array[arrayIndex++] = item;
                }
            }
            ReleaseLock();
        }

        #endregion
    }
}